/* Copyright (c) 2006, 2009, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */
 
package com.cburch.draw.model;

import java.awt.Graphics;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.cburch.draw.canvas.AttributeMapKey;
import com.cburch.draw.canvas.CanvasModel;
import com.cburch.draw.canvas.CanvasModelEvent;
import com.cburch.draw.canvas.CanvasModelListener;
import com.cburch.draw.canvas.CanvasObject;
import com.cburch.draw.canvas.Selection;
import com.cburch.draw.undo.Action;
import com.cburch.draw.undo.UndoLog;
import com.cburch.logisim.data.Attribute;
import com.cburch.logisim.data.Bounds;
import com.cburch.logisim.data.Location;
import com.cburch.logisim.collections.EventSourceWeakSupport;

public class Drawing implements CanvasModel {
    private EventSourceWeakSupport listeners;
    private ArrayList canvasObjects;
    private UndoLog undoLog;
    
	/**
	 * Constructor takes no arguments. Initializes listeners, an ArrayList to store
	 * objects on the canvas and the undoLog;
	 */    
    public Drawing() {
        listeners = new EventSourceWeakSupport();
        canvasObjects = new ArrayList();
        undoLog = new UndoLog();
    }
    
	/**
	 * Adds a CanvasModelListener to the drawing.
	 *
	 * @param	CanvasModelListener		listener to be added to the listener list
	 * @return	void
	 */
    public void addCanvasModelListener(CanvasModelListener l) {
        listeners.add(l);
    }
    
	/**
	 * Removes a CanvasModelListener from the drawing.
	 *
	 * @param	CanvasModelListener		listener to be removed to the listener list
	 * @return	void
	 */
    public void removeCanvasModelListener(CanvasModelListener l) {
        listeners.remove(l);
    }

	/**
	 * Creates a CanvasModelEvent for each listener in the Circuits listener list 
	 * with the given parameters and calls each listeners modelChanged() method.
	 * This particular method creates events for changes to a collection of objects.
	 *
	 * @param	int		
	 * @param	Collection		
	 * @param	int		
	 * @param	int		
	 * @param	int		
	 * @return	void
	 */
    private void fireChanged(int action, Collection affected,
            int index, int dx, int dy) {
        CanvasModelEvent e = null;
        for(Iterator it = listeners.iterator(); it.hasNext(); ) {
            CanvasModelListener l = (CanvasModelListener) it.next();
            if(e == null) {
                e = new CanvasModelEvent(this, action, affected, index, dx, dy);
            }
            l.modelChanged(e);
        }
    }

	/**
	 * Creates a CanvasModelEvent for each listener in the Circuits listener list 
	 * with the given parameters and calls each listeners modelChanged() method. 
	 * This particular method creates events for changes to a single CanvasObject.
	 *
	 * @param	int		
	 * @param	CanvasObject		
	 * @param	int		
	 * @param	int		
	 * @param	int		
	 * @return	void
	 */
    private void fireChanged(int action, CanvasObject affected,
            int index, int dx, int dy) {
        CanvasModelEvent e = null;
        for(Iterator it = listeners.iterator(); it.hasNext(); ) {
            CanvasModelListener l = (CanvasModelListener) it.next();
            if(e == null) {
                Collection coll = Collections.singleton(affected);
                e = new CanvasModelEvent(this, action, coll, index, dx, dy);
            }
            l.modelChanged(e);
        }
    }

	/**
	 * Creates a CanvasModelEvent for each listener in the Circuits listener list 
	 * with the given parameters and calls each listeners modelChanged() method. 
	 * This particular method is specialized to create CanvasModelEvents that
	 * involve changing attribute sets.
	 *
	 * @param	int		
	 * @param	Map		
	 * @param	Map
	 * @return	void
	 */
    private void fireChanged(int action, Map oldValues, Map newValues) {
        CanvasModelEvent e = null;
        for(Iterator it = listeners.iterator(); it.hasNext(); ) {
            CanvasModelListener l = (CanvasModelListener) it.next();
            if(e == null) {
                e = new CanvasModelEvent(this, action, oldValues, newValues);
            }
            l.modelChanged(e);
        }
    }

	/**
	 * Draws a selection of shape objects on the given Graphics object. If a
	 * shape in the selection is suppressed draw it that way.
	 *
	 * @param	Graphics
	 * @param	Selection		
	 * @return	void
	 */
    public void paint(Graphics g, Selection selection) {
        Set suppressed = selection.getDrawsSuppressed();
        for(int i = 0, n = canvasObjects.size(); i < n; i++) {
            CanvasObject shape = (CanvasObject) canvasObjects.get(i);
            Graphics dup = g.create();
            if(suppressed.contains(shape)) {
                selection.drawSuppressed(dup, shape, 0, 0);
            } else {
                shape.draw(dup, 0, 0);
            }
            dup.dispose();
        }
    }

	/**
	 * Attempts to get an object at a specific location.
	 * 
	 * @param	int		x location of possible object
	 * @param	int		y location of possible object
	 * @return	CanvasObject		object at the location, if no object is exists at point then null
	 */
    public CanvasObject getObjectAt(int x, int y) {
        Location loc = Location.create(x, y);
        for(int i = canvasObjects.size() - 1; i >= 0; i--) {
            CanvasObject shape = (CanvasObject) canvasObjects.get(i);
            if(shape.contains(loc)) return shape;
        }
        return null;
    }
    
    /**
	 * Attempts to get all objects within a bounds.
	 * 
	 * @param	Bounds		area to search within for objects
	 * @return	Collection		a list of objects within the given bounds,
	 * 	if no objects are in the bounds returns an empty list
	 */
    public Collection getObjectsIn(Bounds bds) {
        ArrayList ret = null;
        for(int i = 0, n = canvasObjects.size(); i < n; i++) {
            CanvasObject shape = (CanvasObject) canvasObjects.get(i);
            if(bds.contains(shape.getBounds())) {
                if(ret == null) ret = new ArrayList();
                ret.add(shape);
            }
        }
        return ret == null ? Collections.EMPTY_LIST : ret;
    }
    
    /**
	 * Starts the execution of an action by passing an action to the undoLog.
	 * The undoLog will then execute the action and record the event in its
	 * log.
	 * 
	 * @param	Action		the action to be executed
	 * @return	void
	 */
    public void doAction(Action action) {
        undoLog.doAction(action);
    }

	/**
	 * Add all shapes in collection to the drawing. If shapes are successfully added,
	 * fires an CanvasModelEvent. 
	 * 
	 * @param	Collection		collection of the shapes to be added
	 * @return	void
	 */
    public void addObjects(Collection shapes) {
        ArrayList added = new ArrayList(shapes.size());
        for(Iterator it = shapes.iterator(); it.hasNext(); ) {
            Object value = it.next();
            if(!canvasObjects.contains(value)) {
                canvasObjects.add(value);
                added.add(value);
            }
        }
        if(!added.isEmpty()) {
            fireChanged(CanvasModelEvent.ACTION_ADDED, added, 0, 0, 0);
        }
    }
    
	/**
	 * Removes all shapes in a collection from the drawing. If shapes are successfully
	 * removed, fires a CanvasModelEvent.
	 *
	 * @param	Collection		collection of the shapes to be removed
	 * @return	void
	 */
    public void removeObjects(Collection shapes) {
        ArrayList removed = new ArrayList(shapes.size());
        for(Iterator it = shapes.iterator(); it.hasNext(); ) {
            Object value = it.next();
            if(canvasObjects.remove(it.next())) {
                removed.add(value);
            }
        }
        if(!removed.isEmpty()) {
            fireChanged(CanvasModelEvent.ACTION_REMOVED, removed, 0, 0, 0);
        }
    }
    
	/**
	 * Translate all shapes in a collection in the drawing by a 2D vector. If shapes
	 * are successfully translated, fires a CanvasModelEvent.
	 *
	 * @param 	CanvasModelListener		listener to be added to the listener list
	 * @param	int		amount to translate in the x direction
	 * @param	int		amount to translate in the y direction
	 * @return  void
	 */
    public void translateObjects(Collection shapes, int dx, int dy) {
        if(dx != 0 || dy != 0) {
            boolean found = false;
            for(Iterator it = shapes.iterator(); it.hasNext(); ) {
                DrawingMember shape = (DrawingMember) it.next();
                if(canvasObjects.contains(shape)) {
                    found = true;
                    shape.translate(dx, dy);
                }
            }
            if(found) {
                fireChanged(CanvasModelEvent.ACTION_TRANSLATED, shapes, 0, dx, dy);
            }
        }
    }

	/**
	 * Translates a handle for canvas object. Calls the object's internal
	 * move handle function and passes the handle index to be moved and by 
	 * how much. If successful, fires a CanvasModelEvent.
	 *
	 * @param 	CanvasObject		object with handle to be moved
	 * @param	int		index to insert the handle to be moved
	 * @param	int		amount to translate in x direction
	 * @param	int		amount to translate in y direction
	 * @return  void
	 */
    public void moveHandle(CanvasObject shape, int index, int dx, int dy) {
        if(canvasObjects.contains(shape) && (dx != 0 || dy != 0)) {
            ((DrawingMember) shape).moveHandle(index, dx, dy);
            fireChanged(CanvasModelEvent.ACTION_HANDLE_MOVED, shape, index, dx, dy);
        }
    }

	/**
	 * Inserts a handle for canvas object.. Calls the object's internal
	 * insert handle function and passes the handle index to be inserted
	 * at. Fires a CanvasModelEvent after completion.
	 *
	 * @param 	CanvasObject		object to insert a handle to
	 * @param	int		index to insert the handle at
	 * @return  void
	 */
    public void insertHandle(CanvasObject shape, int index) {
        ((DrawingMember) shape).insertHandle(index);
        fireChanged(CanvasModelEvent.ACTION_HANDLE_INSERTED, shape, index, 0, 0);
    }
    
	/**
	 * Deletes a handle for canvas object. Calls the object's internal
	 * delete handle function and passes the handle index to be deleted.
	 * Fires a CanvasModelEvent after completion.
	 *
	 * @param 	CanvasObject		object with the handle to be deleted
	 * @param	int		index of handle to be deleted
	 * @return  void
	 */
    public void deleteHandle(CanvasObject shape, int index) {
        ((DrawingMember) shape).deleteHandle(index);
        fireChanged(CanvasModelEvent.ACTION_HANDLE_DELETED, shape, index, 0, 0);
    }
    
	/**
	 * Sets the attribute of one or more shapes while keeping a record of its
	 * or their old values. Fires the attributes specialized fireChanged() 
	 * method afterwards.
	 *
	 * @param 	Map		values
	 * @return	void
	 */
    public void setAttributeValues(Map values) {
        HashMap oldValues = new HashMap();
        for(Iterator it = values.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = (Map.Entry) it.next();
            AttributeMapKey key = (AttributeMapKey) entry.getKey();
            Object value = entry.getValue();
            Attribute attr = key.getAttribute();
            DrawingMember shape = (DrawingMember) key.getObject();
            oldValues.put(key, shape.getValue(attr));
            shape.setValue(attr, value);
        }
        fireChanged(CanvasModelEvent.ACTION_ATTRIBUTE_CHANGED, oldValues, values);
    }
}
